<?php

	/**
	 * @see Zend_Auth_Adapter_Interface
	 */
	require_once 'Zend/Auth/Adapter/Interface.php';
	

	class Bike_Auth_Adapter_Mapper implements Zend_Auth_Adapter_Interface
	{        const DEFAULT_MAPPER = 'Bike_Model_Mapper_User';
        const AUTH_CREDENTIAL_MATCH_ALIAS = 'bike_auth_credential_match';

        /**
	     * $_mapper - the mapper to check
	     *
	     * @var Bike_Model_Mapper_Abstract
	     */
        protected $_mapper;

        /**
	     * $_identityColumn - the column to use as the identity
	     *
	     * @var string
	     */
	    protected $_identityColumn = null;

	    /**
	     * $_credentialColumns - columns to be used as the credentials
	     *
	     * @var string
	     */
	    protected $_credentialColumn = null;

	    /**
	     * $_identity - Identity value
	     *
	     * @var string
	     */
	    protected $_identity = null;

	    /**
	     * $_credential - Credential values
	     *
	     * @var string
	     */
	    protected $_credential = null;

	    /**
	     * $_credentialTreatment - Treatment applied to the credential, such as MD5() or PASSWORD()
	     *
	     * @var string
	     */
	    protected $_credentialTreatment = null;

	    /**
	     * $_authenticateResultInfo
	     *
	     * @var array
	     */
	    protected $_authenticateResultInfo = null;

	    /**
	     * $_resultRow - Results of database authentication query
	     *
	     * @var array
	     */
	    protected $_resultRow = null;

	    /**
	     * $_ambiguityIdentity - Flag to indicate same Identity can be used with
	     * different credentials. Default is FALSE and need to be set to true to
	     * allow ambiguity usage.
	     *
	     * @var boolean
	     */
	    protected $_ambiguityIdentity = false;

	    /**
	     * $_select - Bike_Db_Mapper_Select authentication query.
	     *
	     * @var Bike_Db_Mapper_Select
	     */
	    protected $_select;


        /**
	     * __construct() - Sets configuration options
	     *
	     * @param  string|Bike_Model_Mapper_Abstract                   $Mapper
	     * @param  string                   $IdentityColumn
	     * @param  string                   $CredentialColumn
	     * @param  string                   $CredentialTreatment
	     * @return void
	     */
		public function __construct($Mapper = null, $IdentityColumn = null, $CredentialColumn = null, $CredentialTreatment = null)
	    {
	        if (!is_null($Mapper))
	        {
	            $this->setMapper($Mapper);
	        }

	        if (!is_null($IdentityColumn))
	        {
	            $this->setIdentityColumn($IdentityColumn);
	        }

	        if (!is_null($CredentialColumn))
	        {
	            $this->setCredentialColumn($CredentialColumn);
	        }

	        if (!is_null($CredentialTreatment))
	        {
	            $this->setCredentialTreatment($CredentialTreatment);
	        }
	    }

	    protected function getAuthCredentialMatchAlias()
	    {	    	return self::AUTH_CREDENTIAL_MATCH_ALIAS;
	    }

		/**
	     * setMapper() - set the mapper
	     *
	     * @param  string|Bike_Model_Mapper_Abstract
	     * @throws Zend_Auth_Adapter_Exception if mapper is not instance of Bike_Model_Mapper_Abstract
	     * @return Bike_Auth_Adapter_Mapper Provides a fluent interface
	     */
	    public function setMapper($Mapper)
	    {			if(is_string($Mapper) && is_callable(array($Mapper, 'getInstance')))
			{				$Mapper = call_user_func(array($Mapper, 'getInstance'));
			}
			if($Mapper instanceof Bike_Model_Mapper_Abstract && $Mapper instanceof Bike_Auth_Adapter_Mapper_Interface)
			{				$this->_mapper = $Mapper;
				$this->setIdentityColumn($this->_mapper->getIdentityColumn());
	            $this->setCredentialColumn($this->_mapper->getCredentialColumn());
	            $this->setCredentialTreatment($this->_mapper->getCredentialTreatment());
			}
			else
			{				Throw new Zend_Auth_Adapter_Exception('Mapper is not instance of Bike_Model_Mapper_Abstract or Bike_Auth_Adapter_Mapper_Interface');
			}
			return $this;
	    }

	    /**
	     * getMapper() - get the mapper
	     *
	     * @return Bike_Model_Mapper_Abstract
	     */

	    public function getMapper()
	    {	    	if(!$this->_mapper instanceof Bike_Model_Mapper_Abstract)
	    	{	    		$this->setMapper(self::DEFAULT_MAPPER);
	    	}
	    	return $this->_mapper;
	    }

	    /**
	     * setIdentityColumn() - set the column name to be used as the identity column
	     *
	     * @param  string $identityColumn
	     * @return Bike_Auth_Adapter_Mapper Provides a fluent interface
	     */
	    public function setIdentityColumn($identityColumn)
	    {
	        $this->_identityColumn = $identityColumn;
	        return $this;
	    }

	    /**
	     * setCredentialColumn() - set the column name to be used as the credential column
	     *
	     * @param  string $credentialColumn
	     * @return Bike_Auth_Adapter_Mapper Provides a fluent interface
	     */
	    public function setCredentialColumn($credentialColumn)
	    {
	        $this->_credentialColumn = $credentialColumn;
	        return $this;
	    }

	    /**
	     * setCredentialTreatment() - allows the developer to pass a parameterized string that is
	     * used to transform or treat the input credential data.
	     *
	     * In many cases, passwords and other sensitive data are encrypted, hashed, encoded,
	     * obscured, or otherwise treated through some function or algorithm. By specifying a
	     * parameterized treatment string with this method, a developer may apply arbitrary SQL
	     * upon input credential data.
	     *
	     * Examples:
	     *
	     *  'PASSWORD(?)'
	     *  'MD5(?)'
	     *
	     * @param  string $treatment
	     * @return Bike_Auth_Adapter_Mapper Provides a fluent interface
	     */
	    public function setCredentialTreatment($treatment)
	    {
	        $this->_credentialTreatment = $treatment;
	        return $this;
	    }

	    /**
	     * setIdentity() - set the value to be used as the identity
	     *
	     * @param  string $value
	     * @return Bike_Auth_Adapter_Mapper Provides a fluent interface
	     */
	    public function setIdentity($value)
	    {
	        $this->_identity = $value;
	        return $this;
	    }

	    /**
	     * setCredential() - set the credential value to be used, optionally can specify a treatment
	     * to be used, should be supplied in parameterized form, such as 'MD5(?)' or 'PASSWORD(?)'
	     *
	     * @param  string $credential
	     * @return Bike_Auth_Adapter_Mapper Provides a fluent interface
	     */
	    public function setCredential($credential)
	    {
	        $this->_credential = $credential;
	        return $this;
	    }

	    /**
	     * setAmbiguityIdentity() - sets a flag for usage of identical identities
	     * with unique credentials. It accepts integers (0, 1) or boolean (true,
	     * false) parameters. Default is false.
	     *
	     * @param  int|bool $flag
	     * @return Zend_Auth_Adapter_DbTable
	     */
	    public function setAmbiguityIdentity($flag)
	    {
	        if (is_integer($flag)) {
	            $this->_ambiguityIdentity = (1 === $flag ? true : false);
	        } elseif (is_bool($flag)) {
	            $this->_ambiguityIdentity = $flag;
	        }
	        return $this;
	    }
	    /**
	     * getAmbiguityIdentity() - returns TRUE for usage of multiple identical
	     * identies with different credentials, FALSE if not used.
	     *
	     * @return bool
	     */
	    public function getAmbiguityIdentity()
	    {
	        return $this->_ambiguityIdentity;
	    }

	    /**
	     * getAdapter() - get the database adapter to be used for quering
	     *
	     * @return Zend_Db_Adapter_Abstract
	     */
	    public function getAdapter()
	    {	    	return $this->getMapper()->getAdapter();
	    }

	    public function getSelect()
	    {	    	if(is_null($this->_select))
	    	{	    		$this->_select = $this->getMapper()->getAuthenticationSelect();
	    	}
	    	return $this->_select;
	    }

	    /**
	     * authenticate() - defined by Zend_Auth_Adapter_Interface.  This method is called to
	     * attempt an authentication.  Previous to this call, this adapter would have already
	     * been configured with all necessary information to successfully connect to a database
	     * table and attempt to find a record matching the provided identity.
	     *
	     * @throws Zend_Auth_Adapter_Exception if answering the authentication query is impossible
	     * @return Zend_Auth_Result
	     */
	    public function authenticate()
	    {
	        $this->_authenticateSetup();
	    	$Select = $this->_authenticateCreateSelect();
	        $resultIdentities = $this->_authenticateQuerySelect($Select);
	        $resultIdentities->setReadOnly();

	        if (($authResult = $this->_authenticateValidateResultSet($resultIdentities)) instanceof Zend_Auth_Result)
	        {
	            return $authResult;
	        }

	        $validIdentities = array();
            $AuthCredentialMatchColumn = $this->getAuthCredentialMatchAlias();
            foreach ($resultIdentities as $identity)
            {
                if (1 === (int) $identity->$AuthCredentialMatchColumn)
                {
                    $validIdentities[] = $identity;
                }
            }
            $resultIdentities = $validIdentities;
			$authResult = $this->_authenticateValidateResult(array_shift($resultIdentities));
	        return $authResult;
	    }

	    /**
	     * _authenticateSetup() - This method abstracts the steps involved with
	     * making sure that this adapter was indeed setup properly with all
	     * required pieces of information.
	     *
	     * @throws Zend_Auth_Adapter_Exception - in the event that setup was not done properly
	     * @return true
	     */
	    protected function _authenticateSetup()
	    {
	        $exception = null;

	        if ($this->_identityColumn == '')
	        {
	            $exception = 'An identity column must be supplied for the Bike_Auth_Adapter_Mapper authentication adapter.';
	        } elseif ($this->_credentialColumn == '')
	        {
	            $exception = 'A credential column must be supplied for the Bike_Auth_Adapter_Mapper authentication adapter.';
	        } elseif ($this->_identity == '')
	        {
	            $exception = 'A value for the identity was not provided prior to authentication with Bike_Auth_Adapter_Mapper.';
	        } elseif ($this->_credential === null)
	        {
	            $exception = 'A credential value was not provided prior to authentication with Bike_Auth_Adapter_Mapper.';
	        }

	        if (null !== $exception) {
	            /**
	             * @see Zend_Auth_Adapter_Exception
	             */
	            require_once 'Zend/Auth/Adapter/Exception.php';
	            Throw new Zend_Auth_Adapter_Exception($exception);
	        }

	        $this->_authenticateResultInfo = array(
	            'code'     => Zend_Auth_Result::FAILURE,
	            'identity' => $this->_identity,
	            'messages' => array()
	            );

	        return true;
	    }

	    /**
	     * _authenticateCreateSelect() - This method creates a Bike_Db_Mapper_Select object that
	     * is completely configured to be queried against the database.
	     *
	     * @return Bike_Db_Mapper_Select
	     */
	    protected function _authenticateCreateSelect()
	    {
	        // build credential expression
	        if (empty($this->_credentialTreatment) || (strpos($this->_credentialTreatment, '?') === false)) {
	            $this->_credentialTreatment = '?';
	        }

	        $credentialExpression = new Zend_Db_Expr(
	            '(CASE WHEN ' .
	            $this->getAdapter()->quoteInto(
	                $this->getAdapter()->quoteIdentifier($this->_credentialColumn, true)
	                . ' = ' . $this->_credentialTreatment, $this->_credential
	            )
	            . ' THEN 1 ELSE 0 END) AS '
	            . $this->getAdapter()->quoteIdentifier(
	                $this->getAdapter()->foldCase($this->getAuthCredentialMatchAlias())
	            )
	        );

	        // get select
	        $Select = $this->getSelect();
            $Select->columns($credentialExpression);
	        $Select->where($this->getAdapter()->quoteIdentifier($this->_identityColumn, true) . ' = ?', $this->_identity);

	        return $Select;
	    }

	    /**
	     * _authenticateQuerySelect() - This method accepts a Bike_Db_Mapper_Select object and
	     * performs a query against the database with that object.
	     *
	     * @param Bike_Db_Mapper_Select $Select
	     * @throws Zend_Auth_Adapter_Exception - when an invalid select
	     *                                       object is encountered
	     * @return array
	     */
	    protected function _authenticateQuerySelect(Bike_Db_Mapper_Select $Select)
	    {
	        try
	        {
	            $resultIdentities = $this->getMapper()->fetchAll($Select);
	        }
	        catch (Exception $e)
	        {
	            /**
	             * @see Zend_Auth_Adapter_Exception
	             */
	            require_once 'Zend/Auth/Adapter/Exception.php';
	            throw new Zend_Auth_Adapter_Exception('The supplied parameters to Bike_Auth_Adapter_Mapper failed to '
	                                                . 'produce a valid sql statement, please check table and column names '
	                                                . 'for validity.', 0, $e);
	        }
	        return $resultIdentities;
	    }

	    /**
	     * _authenticateValidateResultSet() - This method attempts to make
	     * certain that only one record was returned in the resultset
	     *
	     * @param array $resultIdentities
	     * @return true|Zend_Auth_Result
	     */
	    protected function _authenticateValidateResultSet(Bike_Db_Table_Rowset $resultIdentities)
	    {

	        if (count($resultIdentities) < 1) {
	            $this->_authenticateResultInfo['code'] = Zend_Auth_Result::FAILURE_IDENTITY_NOT_FOUND;
	            $this->_authenticateResultInfo['messages'][] = 'A record with the supplied identity could not be found.';
	            return $this->_authenticateCreateAuthResult();
	        } elseif (count($resultIdentities) > 1 && false === $this->getAmbiguityIdentity()) {
	            $this->_authenticateResultInfo['code'] = Zend_Auth_Result::FAILURE_IDENTITY_AMBIGUOUS;
	            $this->_authenticateResultInfo['messages'][] = 'More than one record matches the supplied identity.';
	            return $this->_authenticateCreateAuthResult();
	        }

	        return true;
	    }

	    /**
	     * _authenticateCreateAuthResult() - Creates a Zend_Auth_Result object from
	     * the information that has been collected during the authenticate() attempt.
	     *
	     * @return Zend_Auth_Result
	     */
	    protected function _authenticateCreateAuthResult()
	    {
	        return new Zend_Auth_Result(
	            $this->_authenticateResultInfo['code'],
	            $this->_authenticateResultInfo['identity'],
	            $this->_authenticateResultInfo['messages']
	            );
	    }

	    /**
	     * _authenticateValidateResult() - This method attempts to validate that
	     * the record in the resultset is indeed a record that matched the
	     * identity provided to this adapter.
	     *
	     * @param array $resultIdentity
	     * @return Zend_Auth_Result
	     */
	    protected function _authenticateValidateResult($resultIdentity)
	    {
	        $AuthCredentialMatchColumn = $this->getAuthCredentialMatchAlias();

	        if (!$resultIdentity instanceof Bike_Model_Abstract || $resultIdentity->$AuthCredentialMatchColumn != '1')
	        {
	            $this->_authenticateResultInfo['code'] = Zend_Auth_Result::FAILURE_CREDENTIAL_INVALID;
	            $this->_authenticateResultInfo['messages'][] = 'Supplied credential is invalid.';
	            return $this->_authenticateCreateAuthResult();
	        }

	        $resultIdentity->__unset($AuthCredentialMatchColumn);
	        $this->_resultRow = $resultIdentity;

	        $this->_authenticateResultInfo['code'] = Zend_Auth_Result::SUCCESS;
	        $this->_authenticateResultInfo['messages'][] = 'Authentication successful.';
	        return $this->_authenticateCreateAuthResult();
	    }

	    /**
	     * getResultRow() - Returns the result row object
	     *
	     * @return Bike_Model_Abstract
	     */
	    public function getResultRow()
	    {	    	return $this->_resultRow;
	    }
	}